Repository navigation
feat(claude-code): emit installationPreference in marketplace.json entries - #41430
devin-ai-integration[bot] wants to merge 14 commits into
Conversation
…tries Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Merging this PR will not alter performance
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nstallation_preference
…values Accept only available, auto_install, or required so a typo returns 422 instead of being silently dropped, and cover the PUT full-replace clearing behavior with a test
|
bugbot run |
|
bugbot run |
…ip check and cover hand-edited rows
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
…ccept the camelCase key, and echo the preference on POST and PUT
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9688a2d. Configure here.
TLDR
Problem this solves:
installationPreferenceHow it solves it:
PluginSpecgains an optionalinstallation_preferencefieldavailable,auto_installorrequiredare accepted, anything else is a 422installationPreferenceper pluginUser Flow
Before: a proxy admin registers an internal plugin and Claude Desktop will not auto-install it
{"name": "my-plugin", "source": {"source": "archive", "url": "https://files.example.com/my-plugin.zip", "sha256": "..."}}and get back 200 with"action": "created"name,source,versionbut nothing about installationAfter: the same admin marks the plugin for auto install and Claude Desktop picks it up
"installation_preference": "auto_install"and get back 200 with"action": "created""installationPreference": "auto_install"A value outside
available,auto_installorrequiredis rejected with 422 rather than silently dropped. The same field is accepted by PUT https://litellm-domain/claude-code/plugins/{name} and echoed back asinstallation_preferenceby GET https://litellm-domain/claude-code/plugins and GET https://litellm-domain/claude-code/plugins/{name}. Entries without it are unchangedRelevant issues
Feature request from a customer (Pylon #8704)
Linear ticket
Resolves LIT-7868
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Last updated: 442ed5a, which adds
installationPreferenceto the exportedMarketplacePluginEntrymodel so a client that validates the catalog asMarketplaceResponsekeeps the field, with a round-trip and an invalid-value unit test. The route still returns its JSON directly, so the served marketplace.json and the OpenAPI snapshot are unchanged. The commit before it, 1c16857, merges main in and carries two lint-only fixes: the marketplace helper is public asget_installation_preferenceand the two call sites that read a stored manifest pass it through aTypeAdapterboundary first, and the audit's worker-kill cell uses the harness'skill_worker. None of these changes the served behavior, so QA and /live-pr-risk carry from 19a6c00, the last commit with a product diff, and the /audit file rerun below is the live proof of the refactored linesTwo proxies booted from this rig, each with
--num_workers 2on a random port and its own fresh Postgres database: the before leg at the merge base 118ce3c on port 31361 and the after leg at this PR's tip 19a6c00 on port 50017. The proxy admin's calls are the curl commands below, run as typed with the leg's master key in$LITELLM_MASTER_KEY. The end-user client is Claude Code v2.1.286 driven interactively under tmux against each proxy's marketplace.json. Claude Desktop is the client that acts oninstallationPreference, and only under managed configuration, so it was not driven here; what it reads is the marketplace.json shown in each leg. Docs: BerriAI/litellm-docs#1497Before (118ce3c)
Register one plugin with
installation_preferenceand one without, read every surface that serves them back, then send a value outside the allowed setClaude Code v2.1.286 under tmux:
/plugin marketplace add http://127.0.0.1:31361/claude-code/marketplace.json, then/plugins,internaltyped into the Discover search, Enter oninternal-auto-pluginReplace the plugin with a body that carries only
sourceAfter (19a6c00)
Register one plugin with
installation_preferenceand one without, read every surface that serves them back, then send a value outside the allowed setClaude Code v2.1.286 under tmux:
/plugin marketplace add http://127.0.0.1:50017/claude-code/marketplace.json, then/plugins,internaltyped into the Discover search, Enter oninternal-auto-pluginReplace the plugin with a body that carries only
sourceDependents driven live (/live-pr-risk)
The bundled Admin UI, logged in as the admin on each leg, at
/ui/skillsand theinternal-auto-plugindetail page. Both legs render the same rows and fields; the page reads name, version, description, source and id, never the preference. The before leg listsbad-pluginbecause that proxy accepted the invalid valueMerge-ref check: this PR merged onto origin/main 6fd9334 (a throwaway merge commit, tree e1eabc8962) booted the same way on port 49052, and the whole command sequence above produced the same output as the after leg, the generated plugin id aside
Observations from the legs
/audit: integration cells, base 118ce3c vs head 1c16857 (rows carried from 1ec6486, the file rerun green at 1c16857)
One checked-in test per inventory row in
tests/integration/management/test_claude_code_plugin_installation_preference.py(groupmanagement), run against a real proxy rig: owned Redis, a fresh Postgres database per leg, the scripted upstream fromtests/integration/_support/upstream.py, and per leg two proxy processes sharing that database (primary and peer) booted from the leg's commit, no mocks inside the proxy. The chaos rows boot their own two-worker proxies throughowned_proxy_process. Command per run:python tests/integration/run.py management --seed 4106601 --order-seed 0 <file>managementgroupINTEGRATION_PEER_URLunset), so rows 3, 17, 19, and 20 never ran; row 21 PASS, its owned two-worker proxy ready in 178s underINTEGRATION_PROXY_READY_SECONDS=180on a box at 35 GB of swapEvery cell asserts the new field, so every cell is red on the base by construction; the base column names where the base first diverged
test_registered_preference_is_served_on_the_marketplace[available|auto_install|required]installationPreferencetest_preference_echoes_on_list_detail_and_skill_hubtest_peer_process_serves_the_stored_preferencetest_omitted_preference_is_unsettest_explicit_null_preference_is_unsettest_invalid_preference_is_refused_on_register[6 ids]test_invalid_preference_is_refused_on_update[6 ids]test_duplicate_json_key_keeps_the_last_valuetest_repeated_identical_update_is_idempotenttest_duplicate_name_is_refused_and_keeps_the_first_preferencetest_update_changes_and_clears_the_preferencetest_update_replaces_the_whole_manifesttest_unauthenticated_requests_are_refusedtest_non_admin_key_cannot_modify_the_catalogtest_disabled_plugin_keeps_its_preference_for_granted_keystest_non_granted_key_cannot_read_a_disabled_plugintest_hand_edited_unknown_preference_is_not_servedtest_openapi_declares_the_preference_enum[3 schemas]test_burst_reads_across_processes_while_the_preference_flipstest_repeated_identical_reads_are_stabletest_registration_burst_survives_a_worker_killtest_registrations_survive_a_proxy_restartmarketplace.jsonserved"installationPreference":"auto_install"with and without a bearer, the plugin archive answered 200 from the same origin,/v1/modelslisted the Claude ids,/v1/messagesanswered; the Claude Desktop half not run, since the installed app is the consumer build, which switches to a gateway only through an MDM-delivered managed profileAudit verdict: PASS at 1c16857 under the /audit cap, with row 23's Claude Desktop half accepted on the Medium caveat below. Rows 1 to 22 are green at this tip across runs 5 and 6, row 21 included (its earlier misses at 1ec6486 were the suite's 70s readiness budget on a loaded box, cleared here with a 180s budget and a pass at 178s). Row 23's gateway half ran live: a LiteLLM proxy at this code behind an HTTPS tailnet origin served the marketplace with the field, the archive, and inference, which is everything Claude Desktop reads; the install itself needs a managed Claude Desktop deployment whose
inferenceGatewayBaseUrlis that origin, which this rig cannot provide. Pre-existing on both legs: a non-admin key gets 401 on/claude-code/plugins/{name}/enableand/disablebut 403 on POST, PUT, and DELETE, and concurrent PUTs on one plugin are last-commit-winsType
🆕 New Feature
Caveats (if any)
Medium
marketplace.jsonwith the field, the archive, and/v1/messages), but the installed Claude Desktop is the consumer build, which switches to a gateway only through an MDM-delivered managed profile, so the install itself is unobserved and is accepted on the assumption that a marketplace served exactly as the doc specifies is honored. The field name, its three values, the archive plussha256rule, and the same-origin rule were checked against Anthropic's Claude Desktop extensions doc on 2026-09-30. Closing it needs a managed Claude Desktop deployment whoseinferenceGatewayBaseUrlis an HTTPS LiteLLM origin that also serves the plugin archiveauto_installandrequiredare accepted on any source type, while Claude Desktop honors them only on anarchivesource with asha256served from its gateway originauto_installis served as marked and silently stays manual install in Claude Desktop. The alternative is a 422 on the mark for a non-archive source. Not done here because Anthropic's doc pins per-plugin marks to archives by example only, so it is left open for the reviewerLow
unit / mcp-serverwent red once at 1c16857 ontest_cold_concurrent_schema_validation_accepts_valid_arguments, not from this PRinstallation_preferenceoutside the three values is not servedmanifest_jsonby hand, since POST and PUT reject it with a 422.marketplace.json, the list, get, and skill hub responses treat it as unset instead of failing the requestFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/ee4e29a1f283440e865ceea49877f0fb
Open in Devin Desktop: https://app.devin.ai/desktop/session/ee4e29a1f283440e865ceea49877f0fb?variant=devin
Note
Medium Risk
Changes public marketplace.json shape and plugin admin APIs; misconfigured auto_install on non-archive sources is allowed but may confuse Claude Desktop clients.
Overview
Adds optional
installation_preference(available,auto_install,required) to Claude Code marketplace plugin registration and updates. Admins set it on POST/PUT/claude-code/plugins; invalid enum values return 422, andauto_install/requiredwithout an archive source plus sha256 return 400.When set, the value is stored in the manifest and surfaced as
installationPreferenceonmarketplace.jsonentries (omitted when unset). The same field is echoed on plugin list, get,/public/skill_hub, and register/update responses. Hand-edited unknown manifest values are treated as unset with a warning rather than failing the catalog.OpenAPI snapshot and dashboard
schema.d.tsare updated; coverage includes new integration and unit tests.Reviewed by Cursor Bugbot for commit 9688a2d. Bugbot is set up for automated code reviews on this repo. Configure here.